/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 1997-2009 Sun Microsystems, Inc.  Portions Copyrighted 2008 Richard Schilling.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License. You can obtain
 * a copy of the License at https://glassfish.dev.java.net/public/CDDL+GPL.html
 * or glassfish/bootstrap/legal/LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 *
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at glassfish/bootstrap/legal/LICENSE.txt.
 * Sun designates this particular file as subject to the "Classpath" exception
 * as provided by Sun in the GPL Version 2 section of the License file that
 * accompanied this code.  If applicable, add the following below the License
 * Header, with the fields enclosed by brackets [] replaced by your own
 * identifying information: "Portions Copyrighted [year]
 * [name of copyright owner]"
 *
 * Contributor(s): Richard Schilling (coderroadie@gmail.com)
 *
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */

package com.sun.mail.imap;

import java.util.Date;
import java.util.Vector;
import java.util.Hashtable;
import java.util.NoSuchElementException;
import java.io.*;

import javax.mail.*;
import javax.mail.event.*;
import javax.mail.internet.*;
import javax.mail.search.*;

import com.sun.mail.util.*;
import com.sun.mail.iap.*;
import com.sun.mail.imap.protocol.*;

/**
 * This class implements an IMAP folder.
 * <p>
 * A closed IMAPFolder object shares a protocol connection with its IMAPStore
 * object. When the folder is opened, it gets its own protocol connection.
 * <p>
 * Applications that need to make use of IMAP-specific features may cast a
 * <code>Folder</code> object to an <code>IMAPFolder</code> object and use the
 * methods on this class. The {@link #getQuota getQuota} and {@link #setQuota
 * setQuota} methods support the IMAP QUOTA extension. Refer to <A
 * HREF="http://www.ietf.org/rfc/rfc2087.txt">RFC 2087</A> for more information.
 * <p>
 * The {@link #getACL getACL}, {@link #addACL addACL}, {@link #removeACL
 * removeACL}, {@link #addRights addRights}, {@link #removeRights removeRights},
 * {@link #listRights listRights}, and {@link #myRights myRights} methods
 * support the IMAP ACL extension. Refer to <A
 * HREF="http://www.ietf.org/rfc/rfc2086.txt">RFC 2086</A> for more information.
 * <p>
 * The {@link #doCommand doCommand} method and
 * {@link IMAPFolder.ProtocolCommand IMAPFolder.ProtocolCommand} interface
 * support use of arbitrary IMAP protocol commands.
 * <p>
 * See the <a href="package-summary.html">com.sun.mail.imap</a> package
 * documentation for further information on the IMAP protocol provider.
 * <p>
 * <strong>WARNING:</strong> The APIs unique to this class should be considered
 * <strong>EXPERIMENTAL</strong>. They may be changed in the future in ways that
 * are incompatible with applications using the current APIs.
 * 
 * @author John Mani
 * @author Bill Shannon
 * @author Jim Glennon
 */

/*
 * The folder object itself serves as a lock for the folder's state
 * EXCEPT for the message cache (see below), typically by using
 * synchronized methods. When checking that a folder is open or
 * closed, the folder's lock must be held. It's important that the
 * folder's lock is acquired before the messageCacheLock (see below).
 * Thus, the locking hierarchy is that the folder lock, while optional,
 * must be acquired before the messageCacheLock, if it's acquired at
 * all. Be especially careful of callbacks that occur while holding
 * the messageCacheLock into (e.g.) superclass Folder methods that are
 * synchronized. Note that methods in IMAPMessage will acquire the
 * messageCacheLock without acquiring the folder lock. <p>
 * 
 * When a folder is opened, it creates a messageCache (a Vector) of
 * empty IMAPMessage objects. Each Message has a messageNumber - which
 * is its index into the messageCache, and a sequenceNumber - which is
 * its IMAP sequence-number. All operations on a Message which involve
 * communication with the server, use the message's sequenceNumber. <p>
 * 
 * The most important thing to note here is that the server can send
 * unsolicited EXPUNGE notifications as part of the responses for "most"
 * commands. Refer RFC2060, sections 5.3 & 5.5 for gory details. Also,
 * the server sends these notifications AFTER the message has been
 * expunged. And once a message is expunged, the sequence-numbers of
 * those messages after the expunged one are renumbered. This essentially
 * means that the mapping between *any* Message and its sequence-number
 * can change in the period when a IMAP command is issued and its responses
 * are processed. Hence we impose a strict locking model as follows: <p>
 * 
 * We define one mutex per folder - this is just a Java Object (named
 * messageCacheLock). Any time a command is to be issued to the IMAP
 * server (i.e., anytime the corresponding IMAPProtocol method is
 * invoked), follow the below style:
 * 
 * synchronized (messageCacheLock) { // ACQUIRE LOCK
 * issue command ()
 * 
 * // The response processing is typically done within
 * // the handleResponse() callback. A few commands (Fetch,
 * // Expunge) return *all* responses and hence their
 * // processing is done here itself. Now, as part of the
 * // processing unsolicited EXPUNGE responses, we renumber
 * // the necessary sequence-numbers. Thus the renumbering
 * // happens within this critical-region, surrounded by
 * // locks.
 * process responses ()
 * } // RELEASE LOCK
 * 
 * This technique is used both by methods in IMAPFolder and by methods
 * in IMAPMessage and other classes that operate on data in the folder.
 * Note that holding the messageCacheLock has the side effect of
 * preventing the folder from being closed, and thus ensuring that the
 * folder's protocol object is still valid. The protocol object should
 * only be accessed while holding the messageCacheLock (except for calls
 * to IMAPProtocol.isREV1(), which don't need to be protected because it
 * doesn't access the server).
 * 
 * Note that interactions with the Store's protocol connection do
 * not have to be protected as above, since the Store's protocol is
 * never in a "meaningful" SELECT-ed state.
 */

public class IMAPFolder extends Folder implements UIDFolder, ResponseHandler {

    protected String fullName;		// full name
    protected String name;		// name
    protected int type;			// folder type.
    protected char separator;		// separator
    protected Flags availableFlags; 	// available flags
    protected Flags permanentFlags; 	// permanent flags
    protected boolean exists = false; 	// whether this folder really exists ?
    protected boolean isNamespace = false; // folder is a namespace name
    protected String[] attributes;	// name attributes from LIST response

    protected IMAPProtocol protocol; 	// this folder's own protocol object
    protected MessageCache messageCache;// message cache
    // accessor lock for message cache
    protected final Object messageCacheLock = new Object();

    protected Hashtable uidTable;	// UID->Message hashtable

    /*
     * An IMAP delimiter is a 7bit US-ASCII character. (except NUL).
     * We use '\uffff' (a non 7bit character) to indicate that we havent
     * yet determined what the separator character is.
     * We use '\u0000' (NUL) to indicate that no separator character
     * exists, i.e., a flat hierarchy
     */
    static final protected char UNKNOWN_SEPARATOR = '\uffff';

    private boolean opened = false; 	// is this folder opened ?

    /*
     * This field tracks the state of this folder. If the folder is closed
     * due to external causes (i.e, not thru the close() method), then
     * this field will remain false. If the folder is closed thru the
     * close() method, then this field is set to true.
     * 
     * If reallyClosed is false, then a FolderClosedException is
     * generated when a method is invoked on any Messaging object
     * owned by this folder. If reallyClosed is true, then the
     * IllegalStateException runtime exception is thrown.
     */
    private boolean reallyClosed = true;

    /*
     * The idleState field supports the IDLE command.
     * Normally when executing an IMAP command we hold the
     * messageCacheLock and often the folder lock (see above).
     * While executing the IDLE command we can't hold either
     * of these locks or it would prevent other threads from
     * entering Folder methods even far enough to check whether
     * an IDLE command is in progress. We need to check before
     * issuing another command so that we can abort the IDLE
     * command.
     * 
     * The idleState field is protected by the messageCacheLock.
     * The RUNNING state is the normal state and means no IDLE
     * command is in progress. The IDLE state means we've issued
     * an IDLE command and are reading responses. The ABORTING
     * state means we've sent the DONE continuation command and
     * are waiting for the thread running the IDLE command to
     * break out of its read loop.
     * 
     * When an IDLE command is in progress, the thread calling
     * the idle method will be reading from the IMAP connection
     * while holding neither the folder lock nor the messageCacheLock.
     * It's obviously critical that no other thread try to send a
     * command or read from the connection while in this state.
     * However, other threads can send the DONE continuation
     * command that will cause the server to break out of the IDLE
     * loop and send the ending tag response to the IDLE command.
     * The thread in the idle method that's reading the responses
     * from the IDLE command will see this ending response and
     * complete the idle method, setting the idleState field back
     * to RUNNING, and notifying any threads waiting to use the
     * connection.
     * 
     * All uses of the IMAP connection (IMAPProtocol object) must
     * be done while holding the messageCacheLock and must be
     * preceeded by a check to make sure an IDLE command is not
     * running, and abort the IDLE command if necessary. While
     * waiting for the IDLE command to complete, these other threads
     * will give up the messageCacheLock, but might still be holding
     * the folder lock. This check is done by the getProtocol()
     * method, resulting in a typical usage pattern of:
     * 
     * synchronized (messageCacheLock) {
     * IMAPProtocol p = getProtocol(); // may block waiting for IDLE
     * // ... use protocol
     * }
     */
    private static final int RUNNING = 0;	// not doing IDLE command
    private static final int IDLE = 1;		// IDLE command in effect
    private static final int ABORTING = 2;	// IDLE command aborting
    private int idleState = RUNNING;

    private int total = -1;		// total number of messages in the
    // message cache
    private int recent = -1;		// number of recent messages
    private int realTotal = -1;		// total number of messages on
    // the server
    private long uidvalidity = -1;	// UIDValidity
    private long uidnext = -1;		// UIDNext
    private boolean doExpungeNotification = true; // used in expunge handler

    private Status cachedStatus = null;
    private long cachedStatusTime = 0;

    private boolean hasMessageCountListener = false;	// optimize notification

    private boolean debug = false;
    private PrintStream out;		// debug output stream

    private boolean connectionPoolDebug;

    /**
     * A fetch profile item for fetching headers.
     * This inner class extends the <code>FetchProfile.Item</code> class to add
     * new FetchProfile item types, specific to IMAPFolders.
     * 
     * @see FetchProfile
     */
    public static class FetchProfileItem extends FetchProfile.Item {

        protected FetchProfileItem(String name) {
            super(name);
        }

        /**
         * HEADERS is a fetch profile item that can be included in a
         * <code>FetchProfile</code> during a fetch request to a Folder.
         * This item indicates that the headers for messages in the specified
         * range are desired to be prefetched.
         * <p>
         * An example of how a client uses this is below:
         * <p>
         * <blockquote>
         * 
         * <pre>
         * 
         * FetchProfile fp = new FetchProfile();
         * fp.add(IMAPFolder.FetchProfileItem.HEADERS);
         * folder.fetch(msgs, fp);
         * 
         * </pre>
         * 
         * </blockquote>
         * <p>
         */
        public static final FetchProfileItem HEADERS = new FetchProfileItem("HEADERS");

        /**
         * SIZE is a fetch profile item that can be included in a
         * <code>FetchProfile</code> during a fetch request to a Folder.
         * This item indicates that the sizes of the messages in the specified
         * range are desired to be prefetched.
         * <p>
         * SIZE should move to FetchProfile.Item in JavaMail 1.3.
         */
        public static final FetchProfileItem SIZE = new FetchProfileItem("SIZE");
    }

    /**
     * Constructor used to create a possibly non-existent folder.
     * 
     * @param fullName fullname of this folder
     * @param separator the default separator character for this
     *            folder's namespace
     * @param store the Store
     */
    protected IMAPFolder(String fullName, char separator, IMAPStore store) {
        super(store);
        if (fullName == null)
            throw new NullPointerException("Folder name is null");
        this.fullName = fullName;
        this.separator = separator;
        debug = store.getSession().getDebug();
        connectionPoolDebug = ((IMAPStore) store).getConnectionPoolDebug();
        out = store.getSession().getDebugOut();
        if (out == null)	// should never happen
            out = System.out;

        /*
         * Work around apparent bug in Exchange. Exchange
         * will return a name of "Public Folders/" from
         * LIST "%".
         * 
         * If name has one separator, and it's at the end,
         * assume this is a namespace name and treat it
         * accordingly. Usually this will happen as a result
         * of the list method, but this also allows getFolder
         * to work with namespace names.
         */
        this.isNamespace = false;
        if (separator != UNKNOWN_SEPARATOR && separator != '\0') {
            int i = this.fullName.indexOf(separator);
            if (i > 0 && i == this.fullName.length() - 1) {
                this.fullName = this.fullName.substring(0, i);
                this.isNamespace = true;
            }
        }
    }

    /**
     * Constructor used to create a possibly non-existent folder.
     * 
     * @param fullName fullname of this folder
     * @param separator the default separator character for this
     *            folder's namespace
     * @param store the Store
     */
    protected IMAPFolder(String fullName, char separator, IMAPStore store, boolean isNamespace) {
        this(fullName, separator, store);
        this.isNamespace = isNamespace;
    }

    /**
     * Constructor used to create an existing folder.
     */
    protected IMAPFolder(ListInfo li, IMAPStore store) {
        this(li.name, li.separator, store);

        if (li.hasInferiors)
            type |= HOLDS_FOLDERS;
        if (li.canOpen)
            type |= HOLDS_MESSAGES;
        exists = true;
        attributes = li.attrs;
    }

    /*
     * Ensure that this folder exists. If 'exists' has been set to true,
     * we don't attempt to validate it with the server again. Note that
     * this can result in a possible loss of sync with the server.
     * ASSERT: Must be called with this folder's synchronization lock held.
     */
    private void checkExists() throws MessagingException {
        // If the boolean field 'exists' is false, check with the
        // server by invoking exists() ..
        if (!exists && !exists())
            throw new FolderNotFoundException(this, fullName + " not found");
    }

    /*
     * Ensure the folder is closed.
     * ASSERT: Must be called with this folder's synchronization lock held.
     */
    private void checkClosed() {
        if (opened)
            throw new IllegalStateException("This operation is not allowed on an open folder");
    }

    /*
     * Ensure the folder is open.
     * ASSERT: Must be called with this folder's synchronization lock held.
     */
    private void checkOpened() throws FolderClosedException {
        assert Thread.holdsLock(this);
        if (!opened) {
            if (reallyClosed)
                throw new IllegalStateException("This operation is not allowed on a closed folder");
            else
                // Folder was closed "implicitly"
                throw new FolderClosedException(this, "Lost folder connection to server");
        }
    }

    /*
     * Check that the given message number is within the range
     * of messages present in this folder. If the message
     * number is out of range, we ping the server to obtain any
     * pending new message notifications from the server.
     */
    private void checkRange(int msgno) throws MessagingException {
        if (msgno < 1) // message-numbers start at 1
            throw new IndexOutOfBoundsException("message number < 1");

        if (msgno <= total)
            return;

        // Out of range, let's ping the server and see if
        // the server has more messages for us.

        synchronized (messageCacheLock) { // Acquire lock
            try {
                keepConnectionAlive(false);
            } catch (ConnectionException cex) {
                // Oops, lost connection
                throw new FolderClosedException(this, cex.getMessage());
            } catch (ProtocolException pex) {
                throw new MessagingException(pex.getMessage(), pex);
            }
        } // Release lock

        if (msgno > total) // Still out of range ? Throw up ...
            throw new IndexOutOfBoundsException(msgno + " > " + total);
    }

    /*
     * Check whether the given flags are supported by this server,
     * and also verify that the folder allows setting flags.
     */
    private void checkFlags(Flags flags) throws MessagingException {
        assert Thread.holdsLock(this);
        if (mode != READ_WRITE)
            throw new IllegalStateException("Cannot change flags on READ_ONLY folder: " + fullName);
        /*
         * if (!availableFlags.contains(flags))
         * throw new MessagingException(
         * "These flags are not supported by this implementation"
         * );
         */
    }

    /**
     * Get the name of this folder.
     */
    public synchronized String getName() {
        /*
         * Return the last component of this Folder's full name.
         * Folder components are delimited by the separator character.
         */
        if (name == null) {
            try {
                name = fullName.substring(fullName.lastIndexOf(getSeparator()) + 1);
            } catch (MessagingException mex) {}
        }
        return name;
    }

    /**
     * Get the fullname of this folder.
     */
    public synchronized String getFullName() {
        return fullName;
    }

    /**
     * Get this folder's parent.
     */
    public synchronized Folder getParent() throws MessagingException {
        char c = getSeparator();
        int index;
        if ((index = fullName.lastIndexOf(c)) != -1)
            return new IMAPFolder(fullName.substring(0, index), c, (IMAPStore) store);
        else
            return new DefaultFolder((IMAPStore) store);
    }

    /**
     * Check whether this folder really exists on the server.
     */
    public synchronized boolean exists() throws MessagingException {
        // Check whether this folder exists ..
        ListInfo[] li = null;
        final String lname;
        if (isNamespace && separator != '\0')
            lname = fullName + separator;
        else
            lname = fullName;

        li = (ListInfo[]) doCommand(new ProtocolCommand() {

            public Object doCommand(IMAPProtocol p) throws ProtocolException {
                return p.list("", lname);
            }
        });

        if (li != null) {
            int i = findName(li, lname);
            fullName = li[i].name;
            separator = li[i].separator;
            int len = fullName.length();
            if (separator != '\0' && len > 0 && fullName.charAt(len - 1) == separator) {
                fullName = fullName.substring(0, len - 1);
            }
            type = 0;
            if (li[i].hasInferiors)
                type |= HOLDS_FOLDERS;
            if (li[i].canOpen)
                type |= HOLDS_MESSAGES;
            exists = true;
            attributes = li[i].attrs;
        } else {
            exists = opened;
            attributes = null;
        }

        return exists;
    }

    /**
     * Which entry in <code>li</code> matches <code>lname</code>?
     * If the name contains wildcards, more than one entry may be
     * returned.
     */
    private int findName(ListInfo[] li, String lname) {
        int i;
        // if the name contains a wildcard, there might be more than one
        for (i = 0; i < li.length; i++) {
            if (li[i].name.equals(lname))
                break;
        }
        if (i >= li.length) {	// nothing matched exactly
            // XXX - possibly should fail? But what if server
            // is case insensitive and returns the preferred
            // case of the name here?
            i = 0;		// use first one
        }
        return i;
    }

    /**
     * List all subfolders matching the specified pattern.
     */
    public Folder[] list(String pattern) throws MessagingException {
        return doList(pattern, false);
    }

    /**
     * List all subscribed subfolders matching the specified pattern.
     */
    public Folder[] listSubscribed(String pattern) throws MessagingException {
        return doList(pattern, true);
    }

    private synchronized Folder[] doList(final String pattern, final boolean subscribed)
            throws MessagingException {
        checkExists(); // insure that this folder does exist.

        // Why waste a roundtrip to the server?
        if (attributes != null && !isDirectory())
            return new Folder[0];

        final char c = getSeparator();

        ListInfo[] li = (ListInfo[]) doCommandIgnoreFailure(new ProtocolCommand() {

            public Object doCommand(IMAPProtocol p) throws ProtocolException {
                if (subscribed)
                    return p.lsub("", fullName + c + pattern);
                else
                    return p.list("", fullName + c + pattern);
            }
        });

        if (li == null)
            return new Folder[0];

        /*
         * The UW based IMAP4 servers (e.g. SIMS2.0) include
         * current folder (terminated with the separator), when
         * the LIST pattern is '%' or '*'. i.e, <LIST "" mail/%>
         * returns "mail/" as the first LIST response.
         * 
         * Doesn't make sense to include the current folder in this
         * case, so we filter it out. Note that I'm assuming that
         * the offending response is the *first* one, my experiments
         * with the UW & SIMS2.0 servers indicate that ..
         */
        int start = 0;
        // Check the first LIST response.
        if (li.length > 0 && li[0].name.equals(fullName + c))
            start = 1; // start from index = 1

        IMAPFolder[] folders = new IMAPFolder[li.length - start];
        for (int i = start; i < li.length; i++)
            folders[i - start] = new IMAPFolder(li[i], (IMAPStore) store);
        return folders;
    }

    /**
     * Get the separator character.
     */
    public synchronized char getSeparator() throws MessagingException {
        if (separator == UNKNOWN_SEPARATOR) {
            ListInfo[] li = null;

            li = (ListInfo[]) doCommand(new ProtocolCommand() {

                public Object doCommand(IMAPProtocol p) throws ProtocolException {
                    // REV1 allows the following LIST format to obtain
                    // the hierarchy delimiter of non-existent folders
                    if (p.isREV1()) // IMAP4rev1
                        return p.list(fullName, "");
                    else
                        // IMAP4, note that this folder must exist for this
                        // to work :(
                        return p.list("", fullName);
                }
            });

            if (li != null)
                separator = li[0].separator;
            else
                separator = '/'; // punt !
        }
        return separator;
    }

    /**
     * Get the type of this folder.
     */
    public synchronized int getType() throws MessagingException {
        if (opened) {
            // never throw FolderNotFoundException if folder is open
            if (attributes == null)
                exists();	// try to fetch attributes
        } else {
            checkExists();
        }
        return type;
    }

    /**
     * Check whether this folder is subscribed.
     * <p>
     */
    public synchronized boolean isSubscribed() {
        ListInfo[] li = null;
        final String lname;
        if (isNamespace && separator != '\0')
            lname = fullName + separator;
        else
            lname = fullName;

        try {
            li = (ListInfo[]) doProtocolCommand(new ProtocolCommand() {

                public Object doCommand(IMAPProtocol p) throws ProtocolException {
                    return p.lsub("", lname);
                }
            });
        } catch (ProtocolException pex) {}

        if (li != null) {
            int i = findName(li, lname);
            return li[i].canOpen;
        } else
            return false;
    }

    /**
     * Subscribe/Unsubscribe this folder.
     */
    public synchronized void setSubscribed(final boolean subscribe) throws MessagingException {
        doCommandIgnoreFailure(new ProtocolCommand() {

            public Object doCommand(IMAPProtocol p) throws ProtocolException {
                if (subscribe)
                    p.subscribe(fullName);
                else
                    p.unsubscribe(fullName);
                return null;
            }
        });
    }

    /**
     * Create this folder, with the specified type.
     */
    public synchronized boolean create(final int type) throws MessagingException {

        char c = 0;
        if ((type & HOLDS_MESSAGES) == 0)	// only holds folders
            c = getSeparator();
        final char sep = c;
        Object ret = doCommandIgnoreFailure(new ProtocolCommand() {

            public Object doCommand(IMAPProtocol p) throws ProtocolException {
                if ((type & HOLDS_MESSAGES) == 0)	// only holds folders
                    p.create(fullName + sep);
                else {
                    p.create(fullName);

                    // Certain IMAP servers do not allow creation of folders
                    // that can contain messages *and* subfolders. So, if we
                    // were asked to create such a folder, we should verify
                    // that we could indeed do so.
                    if ((type & HOLDS_FOLDERS) != 0) {
                        // we want to hold subfolders and messages. Check
                        // whether we could create such a folder.
                        ListInfo[] li = p.list("", fullName);
                        if (li != null && !li[0].hasInferiors) {
                            // Hmm ..the new folder
                            // doesn't support Inferiors ? Fail
                            p.delete(fullName);
                            throw new ProtocolException("Unsupported type");
                        }
                    }
                }
                return Boolean.TRUE;
            }
        });

        if (ret == null)
            return false; // CREATE failure, maybe this
        // folder already exists ?

        // exists = true;
        // this.type = type;
        boolean retb = exists();	// set exists, type, and attributes
        if (retb)		// Notify listeners on self and our Store
            notifyFolderListeners(FolderEvent.CREATED);
        return retb;
    }

    /**
     * Check whether this folder has new messages.
     */
    public synchronized boolean hasNewMessages() throws MessagingException {
        if (opened) {	// If we are open, we already have this information
            // Folder is open, make sure information is up to date
            synchronized (messageCacheLock) {
                // tickle the folder and store connections.
                try {
                    keepConnectionAlive(true);
                } catch (ConnectionException cex) {
                    throw new FolderClosedException(this, cex.getMessage());
                } catch (ProtocolException pex) {
                    throw new MessagingException(pex.getMessage(), pex);
                }
                return recent > 0 ? true : false;
            }
        }

        // First, the cheap way - use LIST and look for the \Marked
        // or \Unmarked tag

        ListInfo[] li = null;
        final String lname;
        if (isNamespace && separator != '\0')
            lname = fullName + separator;
        else
            lname = fullName;
        li = (ListInfo[]) doCommandIgnoreFailure(new ProtocolCommand() {

            public Object doCommand(IMAPProtocol p) throws ProtocolException {
                return p.list("", lname);
            }
        });

        // if folder doesn't exist, throw exception
        if (li == null)
            throw new FolderNotFoundException(this, fullName + " not found");

        int i = findName(li, lname);
        if (li[i].changeState == ListInfo.CHANGED)
            return true;
        else if (li[i].changeState == ListInfo.UNCHANGED)
            return false;

        // LIST didn't work. Try the hard way, using STATUS
        try {
            Status status = getStatus();
            if (status.recent > 0)
                return true;
            else
                return false;
        } catch (BadCommandException bex) {
            // Probably doesn't support STATUS, tough luck.
            return false;
        } catch (ConnectionException cex) {
            throw new StoreClosedException(store, cex.getMessage());
        } catch (ProtocolException pex) {
            throw new MessagingException(pex.getMessage(), pex);
        }
    }

    /**
     * Get the named subfolder.
     * <p>
     */
    public synchronized Folder getFolder(String name) throws MessagingException {
        // If we know that this folder is *not* a directory, don't
        // send the request to the server at all ...
        if (attributes != null && !isDirectory())
            throw new MessagingException("Cannot contain subfolders");

        char c = getSeparator();
        return new IMAPFolder(fullName + c + name, c, (IMAPStore) store);
    }

    /**
     * Delete this folder.
     */
    public synchronized boolean delete(boolean recurse) throws MessagingException {
        checkClosed(); // insure that this folder is closed.

        if (recurse) {
            // Delete all subfolders.
            Folder[] f = list();
            for (int i = 0; i < f.length; i++)
                f[i].delete(recurse); // ignore intermediate failures
        }

        // Attempt to delete this folder

        Object ret = doCommandIgnoreFailure(new ProtocolCommand() {

            public Object doCommand(IMAPProtocol p) throws ProtocolException {
                p.delete(fullName);
                return Boolean.TRUE;
            }
        });

        if (ret == null)
            // Non-existent folder/No permission ??
            return false;

        // DELETE succeeded.
        exists = false;
        attributes = null;

        // Notify listeners on self and our Store
        notifyFolderListeners(FolderEvent.DELETED);
        return true;
    }

    /**
     * Rename this folder.
     * <p>
     */
    public synchronized boolean renameTo(final Folder f) throws MessagingException {
        checkClosed(); // insure that we are closed.
        checkExists();
        if (f.getStore() != store)
            throw new MessagingException("Can't rename across Stores");

        Object ret = doCommandIgnoreFailure(new ProtocolCommand() {

            public Object doCommand(IMAPProtocol p) throws ProtocolException {
                p.rename(fullName, f.getFullName());
                return Boolean.TRUE;
            }
        });

        if (ret == null)
            return false;

        exists = false;
        attributes = null;
        notifyFolderRenamedListeners(f);
        return true;
    }

    /**
     * Open this folder in the given mode.
     */
    public synchronized void open(int mode) throws MessagingException {
        checkClosed(); // insure that we are not already open

        MailboxInfo mi = null;
        // Request store for our own protocol connection.
        protocol = ((IMAPStore) store).getProtocol(this);

        synchronized (messageCacheLock) { // Acquire messageCacheLock

            /*
             * Add response handler right away so we get any alerts or
             * notifications that occur during the SELECT or EXAMINE.
             * Have to be sure to remove it if we fail to open the
             * folder.
             */
            protocol.addResponseHandler(this);

            try {
                if (mode == READ_ONLY)
                    mi = protocol.examine(fullName);
                else
                    mi = protocol.select(fullName);
            } catch (CommandFailedException cex) {
                /*
                 * Handle SELECT or EXAMINE failure.
                 * Try to figure out why the operation failed so we can
                 * report a more reasonable exception.
                 * 
                 * Will use our existing protocol object.
                 */
                try {
                    checkExists(); // throw exception if folder doesn't exist

                    if ((type & HOLDS_MESSAGES) == 0)
                        throw new MessagingException("folder cannot contain messages");
                    throw new MessagingException(cex.getMessage(), cex);

                } finally {
                    // folder not open, don't keep this information
                    exists = false;
                    attributes = null;
                    type = 0;
                    // connection still good, return it
                    releaseProtocol(true);
                }
                // NOTREACHED
            } catch (ProtocolException pex) {
                // got a BAD or a BYE; connection may be bad, close it
                try {
                    protocol.logout();
                } catch (ProtocolException pex2) {
                    // ignore
                } finally {
                    releaseProtocol(false);
                    throw new MessagingException(pex.getMessage(), pex);
                }
            }

            if (mi.mode != mode) {
                if (mode == READ_WRITE && mi.mode == READ_ONLY
                        && ((IMAPStore) store).allowReadOnlySelect()) {
                    ;		// all ok, allow it
                } else {	// otherwise, it's an error
                    try {
                        // close mailbox and return connection
                        protocol.close();
                        releaseProtocol(true);
                    } catch (ProtocolException pex) {
                        // something went wrong, close connection
                        try {
                            protocol.logout();
                        } catch (ProtocolException pex2) {
                            // ignore
                        } finally {
                            releaseProtocol(false);
                        }
                    } finally {
                        throw new ReadOnlyFolderException(this, "Cannot open in desired mode");
                    }

                }
            }

            // Initialize stuff.
            opened = true;
            reallyClosed = false;
            this.mode = mi.mode;
            availableFlags = mi.availableFlags;
            permanentFlags = mi.permanentFlags;
            total = realTotal = mi.total;
            recent = mi.recent;
            uidvalidity = mi.uidvalidity;
            uidnext = mi.uidnext;

            // Create the message cache of appropriate size
            messageCache = new MessageCache(this, (IMAPStore) store, total);

        } // Release lock

        exists = true;		// if we opened it, it must exist
        attributes = null;	// but we don't yet know its attributes
        type = HOLDS_MESSAGES;	// lacking more info, we know at least this much

        // notify listeners
        notifyConnectionListeners(ConnectionEvent.OPENED);
    }

    /**
     * Prefetch attributes, based on the given FetchProfile.
     */
    public synchronized void fetch(Message[] msgs, FetchProfile fp) throws MessagingException {
        checkOpened();
        IMAPMessage.fetch(this, msgs, fp);
    }

    /**
     * Set the specified flags for the given array of messages.
     */
    public synchronized void setFlags(Message[] msgs, Flags flag, boolean value)
            throws MessagingException {
        checkOpened();
        checkFlags(flag); // validate flags

        if (msgs.length == 0) // boundary condition
            return;

        synchronized (messageCacheLock) {
            try {
                IMAPProtocol p = getProtocol();
                MessageSet[] ms = Utility.toMessageSet(msgs, null);
                if (ms == null)
                    throw new MessageRemovedException("Messages have been removed");
                p.storeFlags(ms, flag, value);
            } catch (ConnectionException cex) {
                throw new FolderClosedException(this, cex.getMessage());
            } catch (ProtocolException pex) {
                throw new MessagingException(pex.getMessage(), pex);
            }
        }
    }

    /**
     * Close this folder.
     */
    public synchronized void close(boolean expunge) throws MessagingException {
        close(expunge, false);
    }

    /**
     * Close this folder without waiting for the server.
     */
    public synchronized void forceClose() throws MessagingException {
        close(false, true);
    }

    /*
     * Common close method.
     */
    private void close(boolean expunge, boolean force) throws MessagingException {
        assert Thread.holdsLock(this);
        synchronized (messageCacheLock) {
            /*
             * If we already know we're closed, this is illegal.
             * Can't use checkOpened() because if we were forcibly
             * closed asynchronously we just want to complete the
             * closing here.
             */
            if (!opened && reallyClosed)
                throw new IllegalStateException("This operation is not allowed on a closed folder");

            reallyClosed = true; // Ok, lets reset

            // Maybe this folder is already closed, or maybe another
            // thread which had the messageCacheLock earlier, found
            // that our server connection is dead and cleaned up
            // everything ..
            if (!opened)
                return;

            try {
                waitIfIdle();
                if (force) {
                    if (debug)
                        out.println("DEBUG: forcing folder " + fullName + " to close");
                    if (protocol != null)
                        protocol.disconnect();
                } else if (((IMAPStore) store).isConnectionPoolFull()) {
                    // If the connection pool is full, logout the connection
                    if (debug)
                        out.println("DEBUG: pool is full, not adding "
                                + "an Authenticated connection");

                    // If the expunge flag is set, close the folder first.
                    if (expunge)
                        protocol.close();

                    if (protocol != null)
                        protocol.logout();
                } else {
                    // If the expunge flag is set or we're open read-only we
                    // can just close the folder, otherwise open it read-only
                    // before closing.
                    if (!expunge && mode == READ_WRITE) {
                        try {
                            MailboxInfo mi = protocol.examine(fullName);
                        } catch (ProtocolException pex2) {
                            if (protocol != null)
                                protocol.disconnect();
                        }
                    }
                    if (protocol != null)
                        protocol.close();
                }
            } catch (ProtocolException pex) {
                throw new MessagingException(pex.getMessage(), pex);
            } finally {
                // cleanup if we haven't already
                if (opened)
                    cleanup(true);
            }
        }
    }

    // NOTE: this method can currently be invoked from close() or
    // from handleResponses(). Both invocations are conditional,
    // based on the "opened" flag, so we are sure that multiple
    // Connection.CLOSED events are not generated. Also both
    // invocations are from within messageCacheLock-ed areas.
    private void cleanup(boolean returnToPool) {
        assert Thread.holdsLock(messageCacheLock);
        releaseProtocol(returnToPool);
        messageCache = null;
        uidTable = null;
        exists = false; // to force a recheck in exists().
        attributes = null;
        opened = false;
        idleState = RUNNING;	// just in case
        notifyConnectionListeners(ConnectionEvent.CLOSED);
    }

    /**
     * Check whether this connection is really open.
     */
    public synchronized boolean isOpen() {
        synchronized (messageCacheLock) {
            // Probe the connection to make sure its really open.
            if (opened) {
                try {
                    keepConnectionAlive(false);
                } catch (ProtocolException pex) {}
            }
        }

        return opened;
    }

    /**
     * Return the permanent flags supported by the server.
     */
    public synchronized Flags getPermanentFlags() {
        return (Flags) (permanentFlags.clone());
    }

    /**
     * Get the total message count.
     */
    public synchronized int getMessageCount() throws MessagingException {
        if (!opened) {
            checkExists();
            // If this folder is not yet open, we use STATUS to
            // get the total message count
            try {
                Status status = getStatus();
                return status.total;
            } catch (BadCommandException bex) {
                // doesn't support STATUS, probably vanilla IMAP4 ..
                // lets try EXAMINE
                IMAPProtocol p = null;

                try {
                    p = getStoreProtocol();	// XXX
                    MailboxInfo minfo = p.examine(fullName);
                    p.close();
                    return minfo.total;
                } catch (ProtocolException pex) {
                    // Give up.
                    throw new MessagingException(pex.getMessage(), pex);
                } finally {
                    releaseStoreProtocol(p);
                }
            } catch (ConnectionException cex) {
                throw new StoreClosedException(store, cex.getMessage());
            } catch (ProtocolException pex) {
                throw new MessagingException(pex.getMessage(), pex);
            }
        }

        // Folder is open, we know what the total message count is ..
        synchronized (messageCacheLock) {
            // tickle the folder and store connections.
            try {
                keepConnectionAlive(true);
                return total;
            } catch (ConnectionException cex) {
                throw new FolderClosedException(this, cex.getMessage());
            } catch (ProtocolException pex) {
                throw new MessagingException(pex.getMessage(), pex);
            }
        }
    }

    /**
     * Get the new message count.
     */
    public synchronized int getNewMessageCount() throws MessagingException {
        if (!opened) {
            checkExists();
            // If this folder is not yet open, we use STATUS to
            // get the new message count
            try {
                Status status = getStatus();
                return status.recent;
            } catch (BadCommandException bex) {
                // doesn't support STATUS, probably vanilla IMAP4 ..
                // lets try EXAMINE
                IMAPProtocol p = null;

                try {
                    p = getStoreProtocol();	// XXX
                    MailboxInfo minfo = p.examine(fullName);
                    p.close();
                    return minfo.recent;
                } catch (ProtocolException pex) {
                    // Give up.
                    throw new MessagingException(pex.getMessage(), pex);
                } finally {
                    releaseStoreProtocol(p);
                }
            } catch (ConnectionException cex) {
                throw new StoreClosedException(store, cex.getMessage());
            } catch (ProtocolException pex) {
                throw new MessagingException(pex.getMessage(), pex);
            }
        }

        // Folder is open, we know what the new message count is ..
        synchronized (messageCacheLock) {
            // tickle the folder and store connections.
            try {
                keepConnectionAlive(true);
                return recent;
            } catch (ConnectionException cex) {
                throw new FolderClosedException(this, cex.getMessage());
            } catch (ProtocolException pex) {
                throw new MessagingException(pex.getMessage(), pex);
            }
        }
    }

    /**
     * Get the unread message count.
     */
    public synchronized int getUnreadMessageCount() throws MessagingException {
        if (!opened) {
            checkExists();
            // If this folder is not yet open, we use STATUS to
            // get the unseen message count
            try {
                Status status = getStatus();
                return status.unseen;
            } catch (BadCommandException bex) {
                // doesn't support STATUS, probably vanilla IMAP4 ..
                // Could EXAMINE, SEARCH for UNREAD messages and
                // return the count .. bah, not worth it.
                return -1;
            } catch (ConnectionException cex) {
                throw new StoreClosedException(store, cex.getMessage());
            } catch (ProtocolException pex) {
                throw new MessagingException(pex.getMessage(), pex);
            }
        }

        // if opened, issue server-side search for messages that do
        // *not* have the SEEN flag.
        Flags f = new Flags();
        f.add(Flags.Flag.SEEN);
        try {
            synchronized (messageCacheLock) {
                int[] matches = getProtocol().search(new FlagTerm(f, false));
                return matches.length; // NOTE: 'matches' is never null
            }
        } catch (ConnectionException cex) {
            throw new FolderClosedException(this, cex.getMessage());
        } catch (ProtocolException pex) {
            // Shouldn't happen
            throw new MessagingException(pex.getMessage(), pex);
        }
    }

    /**
     * Get the deleted message count.
     */
    public synchronized int getDeletedMessageCount() throws MessagingException {
        if (!opened) {
            checkExists();
            // no way to do this on closed folders
            return -1;
        }

        // if opened, issue server-side search for messages that do
        // have the DELETED flag.
        Flags f = new Flags();
        f.add(Flags.Flag.DELETED);
        try {
            synchronized (messageCacheLock) {
                int[] matches = getProtocol().search(new FlagTerm(f, true));
                return matches.length; // NOTE: 'matches' is never null
            }
        } catch (ConnectionException cex) {
            throw new FolderClosedException(this, cex.getMessage());
        } catch (ProtocolException pex) {
            // Shouldn't happen
            throw new MessagingException(pex.getMessage(), pex);
        }
    }

    /*
     * Get results of STATUS command for this folder, checking cache first.
     * ASSERT: Must be called with this folder's synchronization lock held.
     * ASSERT: The folder must be closed.
     */
    private Status getStatus() throws ProtocolException {
        int statusCacheTimeout = ((IMAPStore) store).getStatusCacheTimeout();

        // if allowed to cache and our cache is still valid, return it
        if (statusCacheTimeout > 0 && cachedStatus != null
                && System.currentTimeMillis() - cachedStatusTime < statusCacheTimeout)
            return cachedStatus;

        IMAPProtocol p = null;

        try {
            p = getStoreProtocol();	// XXX
            Status s = p.status(fullName, null);
            // if allowed to cache, do so
            if (statusCacheTimeout > 0) {
                cachedStatus = s;
                cachedStatusTime = System.currentTimeMillis();
            }
            return s;
        } finally {
            releaseStoreProtocol(p);
        }
    }

    /**
     * Get the specified message.
     */
    public synchronized Message getMessage(int msgnum) throws MessagingException {
        checkOpened();
        checkRange(msgnum);

        return messageCache.getMessage(msgnum);
    }

    /**
     * Append the given messages into this folder.
     */
    public synchronized void appendMessages(Message[] msgs) throws MessagingException {
        checkExists(); // verify that self exists

        // XXX - have to verify that messages are in a different
        // store (if any) than target folder, otherwise could
        // deadlock trying to fetch messages on the same connection
        // we're using for the append.

        int maxsize = ((IMAPStore) store).getAppendBufferSize();

        for (int i = 0; i < msgs.length; i++) {
            final Message m = msgs[i];
            Date d = m.getReceivedDate(); // retain dates
            if (d == null)
                d = m.getSentDate();
            final Date dd = d;
            final Flags f = m.getFlags();

            final MessageLiteral mos;
            try {
                // if we know the message is too big, don't buffer any of it
                mos = new MessageLiteral(m, m.getSize() > maxsize ? 0 : maxsize);
            } catch (IOException ex) {
                throw new MessagingException("IOException while appending messages", ex);
            } catch (MessageRemovedException mrex) {
                continue; // just skip this expunged message
            }

            doCommand(new ProtocolCommand() {

                public Object doCommand(IMAPProtocol p) throws ProtocolException {
                    p.append(fullName, f, dd, mos);
                    return null;
                }
            });
        }
    }

    /**
     * Append the given messages into this folder.
     * Return array of AppendUID objects containing
     * UIDs of these messages in the destination folder.
     * Each element of the returned array corresponds to
     * an element of the <code>msgs</code> array. A null
     * element means the server didn't return UID information
     * for the appended message.
     * <p>
     * Depends on the APPENDUID response code defined by the UIDPLUS extension -
     * <A HREF="http://www.ietf.org/rfc/rfc2359.txt">RFC 2359</A>.
     * 
     * @since JavaMail 1.4
     */
    public synchronized AppendUID[] appendUIDMessages(Message[] msgs) throws MessagingException {
        checkExists(); // verify that self exists

        // XXX - have to verify that messages are in a different
        // store (if any) than target folder, otherwise could
        // deadlock trying to fetch messages on the same connection
        // we're using for the append.

        int maxsize = ((IMAPStore) store).getAppendBufferSize();

        AppendUID[] uids = new AppendUID[msgs.length];
        for (int i = 0; i < msgs.length; i++) {
            final Message m = msgs[i];
            final MessageLiteral mos;

            try {
                // if we know the message is too big, don't buffer any of it
                mos = new MessageLiteral(m, m.getSize() > maxsize ? 0 : maxsize);
            } catch (IOException ex) {
                throw new MessagingException("IOException while appending messages", ex);
            } catch (MessageRemovedException mrex) {
                continue; // just skip this expunged message
            }

            Date d = m.getReceivedDate(); // retain dates
            if (d == null)
                d = m.getSentDate();
            final Date dd = d;
            final Flags f = m.getFlags();
            AppendUID auid = (AppendUID) doCommand(new ProtocolCommand() {

                public Object doCommand(IMAPProtocol p) throws ProtocolException {
                    return p.appenduid(fullName, f, dd, mos);
                }
            });
            uids[i] = auid;
        }
        return uids;
    }

    /**
     * Append the given messages into this folder.
     * Return array of Message objects representing
     * the messages in the destination folder. Note
     * that the folder must be open.
     * Each element of the returned array corresponds to
     * an element of the <code>msgs</code> array. A null
     * element means the server didn't return UID information
     * for the appended message.
     * <p>
     * Depends on the APPENDUID response code defined by the UIDPLUS extension -
     * <A HREF="http://www.ietf.org/rfc/rfc2359.txt">RFC 2359</A>.
     * 
     * @since JavaMail 1.4
     */
    public synchronized Message[] addMessages(Message[] msgs) throws MessagingException {
        checkOpened();
        Message[] rmsgs = new MimeMessage[msgs.length];
        AppendUID[] uids = appendUIDMessages(msgs);
        for (int i = 0; i < uids.length; i++) {
            AppendUID auid = uids[i];
            if (auid != null) {
                if (auid.uidvalidity == uidvalidity) {
                    try {
                        rmsgs[i] = getMessageByUID(auid.uid);
                    } catch (MessagingException mex) {
                        // ignore errors at this stage
                    }
                }
            }
        }
        return rmsgs;
    }

    /**
     * Copy the specified messages from this folder, to the
     * specified destination.
     */
    public synchronized void copyMessages(Message[] msgs, Folder folder) throws MessagingException {
        checkOpened();

        if (msgs.length == 0) // boundary condition
            return;

        // If the destination belongs to our same store, optimize
        if (folder.getStore() == store) {
            synchronized (messageCacheLock) {
                try {
                    IMAPProtocol p = getProtocol();
                    MessageSet[] ms = Utility.toMessageSet(msgs, null);
                    if (ms == null)
                        throw new MessageRemovedException("Messages have been removed");
                    p.copy(ms, folder.getFullName());
                } catch (CommandFailedException cfx) {
                    if (cfx.getMessage().indexOf("TRYCREATE") != -1)
                        throw new FolderNotFoundException(folder, folder.getFullName()
                                + " does not exist");
                    else
                        throw new MessagingException(cfx.getMessage(), cfx);
                } catch (ConnectionException cex) {
                    throw new FolderClosedException(this, cex.getMessage());
                } catch (ProtocolException pex) {
                    throw new MessagingException(pex.getMessage(), pex);
                }
            }
        } else
            // destination is a different store.
            super.copyMessages(msgs, folder);
    }

    /**
     * Expunge all messages marked as DELETED.
     */
    public synchronized Message[] expunge() throws MessagingException {
        return expunge(null);
    }

    /**
     * Expunge the indicated messages, which must have been marked as DELETED.
     */
    public synchronized Message[] expunge(Message[] msgs) throws MessagingException {
        checkOpened();

        if (msgs != null) {
            // call fetch to make sure we have all the UIDs
            FetchProfile fp = new FetchProfile();
            fp.add(UIDFolder.FetchProfileItem.UID);
            fetch(msgs, fp);
        }

        IMAPMessage[] rmsgs;
        synchronized (messageCacheLock) {
            doExpungeNotification = false; // We do this ourselves later
            try {
                IMAPProtocol p = getProtocol();
                if (msgs != null)
                    p.uidexpunge(Utility.toUIDSet(msgs));
                else
                    p.expunge();
            } catch (CommandFailedException cfx) {
                // expunge not allowed, perhaps due to a permission problem?
                if (mode != READ_WRITE)
                    throw new IllegalStateException("Cannot expunge READ_ONLY folder: " + fullName);
                else
                    throw new MessagingException(cfx.getMessage(), cfx);
            } catch (ConnectionException cex) {
                throw new FolderClosedException(this, cex.getMessage());
            } catch (ProtocolException pex) {
                // Bad bad server ..
                throw new MessagingException(pex.getMessage(), pex);
            } finally {
                doExpungeNotification = true;
            }

            // Cleanup expunged messages and sync messageCache with reality.
            if (msgs != null)
                rmsgs = messageCache.removeExpungedMessages(msgs);
            else
                rmsgs = messageCache.removeExpungedMessages();
            if (uidTable != null) {
                for (int i = 0; i < rmsgs.length; i++) {
                    IMAPMessage m = rmsgs[i];
                    /* remove this message from the UIDTable */
                    long uid = m.getUID();
                    if (uid != -1)
                        uidTable.remove(new Long(uid));
                }
            }

            // Update 'total'
            total = messageCache.size();
        }

        // Notify listeners. This time its for real, guys.
        if (rmsgs.length > 0)
            notifyMessageRemovedListeners(true, rmsgs);
        return rmsgs;
    }

    /**
     * Search whole folder for messages matching the given term.
     */
    public synchronized Message[] search(SearchTerm term) throws MessagingException {
        checkOpened();

        try {
            Message[] matchMsgs = null;

            synchronized (messageCacheLock) {
                int[] matches = getProtocol().search(term);
                if (matches != null) {
                    matchMsgs = new IMAPMessage[matches.length];
                    // Map seq-numbers into actual Messages.
                    for (int i = 0; i < matches.length; i++)
                        matchMsgs[i] = getMessageBySeqNumber(matches[i]);
                }
            }
            return matchMsgs;

        } catch (CommandFailedException cfx) {
            // unsupported charset or search criterion
            return super.search(term);
        } catch (SearchException sex) {
            // too complex for IMAP
            return super.search(term);
        } catch (ConnectionException cex) {
            throw new FolderClosedException(this, cex.getMessage());
        } catch (ProtocolException pex) {
            // bug in our IMAP layer ?
            throw new MessagingException(pex.getMessage(), pex);
        }
    }

    /**
     * Search the folder for messages matching the given term. Returns
     * array of matching messages. Returns an empty array if no matching
     * messages are found.
     */
    public synchronized Message[] search(SearchTerm term, Message[] msgs) throws MessagingException {
        checkOpened();

        if (msgs.length == 0)
            // need to return an empty array (not null!)
            return msgs;

        try {
            Message[] matchMsgs = null;

            synchronized (messageCacheLock) {
                IMAPProtocol p = getProtocol();
                MessageSet[] ms = Utility.toMessageSet(msgs, null);
                if (ms == null)
                    throw new MessageRemovedException("Messages have been removed");
                int[] matches = p.search(ms, term);
                if (matches != null) {
                    matchMsgs = new IMAPMessage[matches.length];
                    for (int i = 0; i < matches.length; i++)
                        matchMsgs[i] = getMessageBySeqNumber(matches[i]);
                }
            }
            return matchMsgs;

        } catch (CommandFailedException cfx) {
            // unsupported charset or search criterion
            return super.search(term, msgs);
        } catch (SearchException sex) {
            // too complex for IMAP
            return super.search(term, msgs);
        } catch (ConnectionException cex) {
            throw new FolderClosedException(this, cex.getMessage());
        } catch (ProtocolException pex) {
            // bug in our IMAP layer ?
            throw new MessagingException(pex.getMessage(), pex);
        }
    }

    /*
     * Override Folder method to keep track of whether we have any
     * message count listeners. Normally we won't have any, so we
     * can avoid creating message objects to pass to the notify
     * method. It's too hard to keep track of when all listeners
     * are removed, and that's a rare case, so we don't try.
     */
    public synchronized void addMessageCountListener(MessageCountListener l) {
        super.addMessageCountListener(l);
        hasMessageCountListener = true;
    }

    /***********************************************************
     * UIDFolder interface methods
     **********************************************************/

    /**
     * Returns the UIDValidity for this folder.
     */
    public synchronized long getUIDValidity() throws MessagingException {
        if (opened) // we already have this information
            return uidvalidity;

        IMAPProtocol p = null;
        Status status = null;

        try {
            p = getStoreProtocol();	// XXX
            String[] item = { "UIDVALIDITY" };
            status = p.status(fullName, item);
        } catch (BadCommandException bex) {
            // Probably a RFC1730 server
            throw new MessagingException("Cannot obtain UIDValidity", bex);
        } catch (ConnectionException cex) {
            // Oops, the store or folder died on us.
            throwClosedException(cex);
        } catch (ProtocolException pex) {
            throw new MessagingException(pex.getMessage(), pex);
        } finally {
            releaseStoreProtocol(p);
        }

        return status.uidvalidity;
    }

    /**
     * Returns the predicted UID that will be assigned to the
     * next message that is appended to this folder.
     * If the folder is closed, the STATUS command is used to
     * retrieve this value. If the folder is open, the value
     * returned from the SELECT or EXAMINE command is returned.
     * Note that messages may have been appended to the folder
     * while it was open and thus this value may be out of
     * date.
     * <p>
     * Servers implementing RFC2060 likely won't return this value when a folder
     * is opened. Servers implementing RFC3501 should return this value when a
     * folder is opened.
     * <p>
     * 
     * @return the UIDNEXT value, or -1 if unknown
     * @since JavaMail 1.3.3
     */
    // Not a UIDFolder method, but still useful
    public synchronized long getUIDNext() throws MessagingException {
        if (opened) // we already have this information
            return uidnext;

        IMAPProtocol p = null;
        Status status = null;

        try {
            p = getStoreProtocol();	// XXX
            String[] item = { "UIDNEXT" };
            status = p.status(fullName, item);
        } catch (BadCommandException bex) {
            // Probably a RFC1730 server
            throw new MessagingException("Cannot obtain UIDNext", bex);
        } catch (ConnectionException cex) {
            // Oops, the store or folder died on us.
            throwClosedException(cex);
        } catch (ProtocolException pex) {
            throw new MessagingException(pex.getMessage(), pex);
        } finally {
            releaseStoreProtocol(p);
        }

        return status.uidnext;
    }

    /**
     * Get the Message corresponding to the given UID.
     * If no such message exists, <code> null </code> is returned.
     */
    public synchronized Message getMessageByUID(long uid) throws MessagingException {
        checkOpened(); // insure folder is open

        IMAPMessage m = null;

        try {
            synchronized (messageCacheLock) {
                Long l = new Long(uid);

                if (uidTable != null) {
                    // Check in uidTable
                    m = (IMAPMessage) uidTable.get(l);
                    if (m != null) // found it
                        return m;
                } else
                    uidTable = new Hashtable();

                // Check with the server
                // Issue UID FETCH command
                UID u = getProtocol().fetchSequenceNumber(uid);

                if (u != null && u.seqnum <= total) { // Valid UID
                    m = getMessageBySeqNumber(u.seqnum);
                    m.setUID(u.uid); // set this message's UID ..
                    // .. and put this into the hashtable
                    uidTable.put(l, m);
                }
            }
        } catch (ConnectionException cex) {
            throw new FolderClosedException(this, cex.getMessage());
        } catch (ProtocolException pex) {
            throw new MessagingException(pex.getMessage(), pex);
        }

        return m;
    }

    /**
     * Get the Messages specified by the given range.
     * <p>
     * Returns Message objects for all valid messages in this range. Returns an
     * empty array if no messages are found.
     */
    public synchronized Message[] getMessagesByUID(long start, long end) throws MessagingException {
        checkOpened(); // insure that folder is open

        Message[] msgs; // array of messages to be returned

        try {
            synchronized (messageCacheLock) {
                if (uidTable == null)
                    uidTable = new Hashtable();

                // Issue UID FETCH for given range
                UID[] ua = getProtocol().fetchSequenceNumbers(start, end);

                msgs = new Message[ua.length];
                IMAPMessage m;
                // NOTE: Below must be within messageCacheLock region
                for (int i = 0; i < ua.length; i++) {
                    m = getMessageBySeqNumber(ua[i].seqnum);
                    m.setUID(ua[i].uid);
                    msgs[i] = m;
                    uidTable.put(new Long(ua[i].uid), m);
                }
            }
        } catch (ConnectionException cex) {
            throw new FolderClosedException(this, cex.getMessage());
        } catch (ProtocolException pex) {
            throw new MessagingException(pex.getMessage(), pex);
        }

        return msgs;
    }

    /**
     * Get the Messages specified by the given array.
     * <p>
     * <code>uids.length()</code> elements are returned. If any UID in the array
     * is invalid, a <code>null</code> entry is returned for that element.
     */
    public synchronized Message[] getMessagesByUID(long[] uids) throws MessagingException {
        checkOpened(); // insure that folder is open

        try {
            synchronized (messageCacheLock) {
                long[] unavailUids = uids;
                if (uidTable != null) {
                    Vector v = new Vector(); // to collect unavailable UIDs
                    Long l;
                    for (int i = 0; i < uids.length; i++) {
                        if (!uidTable.containsKey(l = new Long(uids[i])))
                            // This UID has not been loaded yet.
                            v.addElement(l);
                    }

                    int vsize = v.size();
                    unavailUids = new long[vsize];
                    for (int i = 0; i < vsize; i++)
                        unavailUids[i] = ((Long) v.elementAt(i)).longValue();
                } else
                    uidTable = new Hashtable();

                if (unavailUids.length > 0) {
                    // Issue UID FETCH request for given uids
                    UID[] ua = getProtocol().fetchSequenceNumbers(unavailUids);
                    IMAPMessage m;
                    for (int i = 0; i < ua.length; i++) {
                        m = getMessageBySeqNumber(ua[i].seqnum);
                        m.setUID(ua[i].uid);
                        uidTable.put(new Long(ua[i].uid), m);
                    }
                }

                // Return array of size = uids.length
                Message[] msgs = new Message[uids.length];
                for (int i = 0; i < uids.length; i++)
                    msgs[i] = (Message) uidTable.get(new Long(uids[i]));
                return msgs;
            }
        } catch (ConnectionException cex) {
            throw new FolderClosedException(this, cex.getMessage());
        } catch (ProtocolException pex) {
            throw new MessagingException(pex.getMessage(), pex);
        }
    }

    /**
     * Get the UID for the specified message.
     */
    public synchronized long getUID(Message message) throws MessagingException {
        if (message.getFolder() != this)
            throw new NoSuchElementException("Message does not belong to this folder");

        checkOpened(); // insure that folder is open

        IMAPMessage m = (IMAPMessage) message;
        // If the message already knows its UID, great ..
        long uid;
        if ((uid = m.getUID()) != -1)
            return uid;

        synchronized (messageCacheLock) { // Acquire Lock
            try {
                IMAPProtocol p = getProtocol();
                m.checkExpunged(); // insure that message is not expunged
                UID u = p.fetchUID(m.getSequenceNumber());

                if (u != null) {
                    uid = u.uid;
                    m.setUID(uid); // set message's UID

                    // insert this message into uidTable
                    if (uidTable == null)
                        uidTable = new Hashtable();
                    uidTable.put(new Long(uid), m);
                }
            } catch (ConnectionException cex) {
                throw new FolderClosedException(this, cex.getMessage());
            } catch (ProtocolException pex) {
                throw new MessagingException(pex.getMessage(), pex);
            }
        }

        return uid;
    }

    /**
     * Get the quotas for the quotaroot associated with this
     * folder. Note that many folders may have the same quotaroot.
     * Quotas are controlled on the basis of a quotaroot, not
     * (necessarily) a folder. The relationship between folders
     * and quotaroots depends on the IMAP server. Some servers
     * might implement a single quotaroot for all folders owned by
     * a user. Other servers might implement a separate quotaroot
     * for each folder. A single folder can even have multiple
     * quotaroots, perhaps controlling quotas for different
     * resources.
     * 
     * @return array of Quota objects for the quotaroots associated with
     *         this folder
     * @exception MessagingException if the server doesn't support the
     *                QUOTA extension
     */
    public Quota[] getQuota() throws MessagingException {
        return (Quota[]) doOptionalCommand("QUOTA not supported", new ProtocolCommand() {

            public Object doCommand(IMAPProtocol p) throws ProtocolException {
                return p.getQuotaRoot(fullName);
            }
        });
    }

    /**
     * Set the quotas for the quotaroot specified in the quota argument.
     * Typically this will be one of the quotaroots associated with this
     * folder, as obtained from the <code>getQuota</code> method, but it
     * need not be.
     * 
     * @param quota the quota to set
     * @exception MessagingException if the server doesn't support the
     *                QUOTA extension
     */
    public void setQuota(final Quota quota) throws MessagingException {
        doOptionalCommand("QUOTA not supported", new ProtocolCommand() {

            public Object doCommand(IMAPProtocol p) throws ProtocolException {
                p.setQuota(quota);
                return null;
            }
        });
    }

    /**
     * Get the access control list entries for this folder.
     * 
     * @return array of access control list entries
     * @exception MessagingException if the server doesn't support the
     *                ACL extension
     */
    public ACL[] getACL() throws MessagingException {
        return (ACL[]) doOptionalCommand("ACL not supported", new ProtocolCommand() {

            public Object doCommand(IMAPProtocol p) throws ProtocolException {
                return p.getACL(fullName);
            }
        });
    }

    /**
     * Add an access control list entry to the access control list
     * for this folder.
     * 
     * @param acl the access control list entry to add
     * @exception MessagingException if the server doesn't support the
     *                ACL extension
     */
    public void addACL(ACL acl) throws MessagingException {
        setACL(acl, '\0');
    }

    /**
     * Remove any access control list entry for the given identifier
     * from the access control list for this folder.
     * 
     * @param name the identifier for which to remove all ACL entries
     * @exception MessagingException if the server doesn't support the
     *                ACL extension
     */
    public void removeACL(final String name) throws MessagingException {
        doOptionalCommand("ACL not supported", new ProtocolCommand() {

            public Object doCommand(IMAPProtocol p) throws ProtocolException {
                p.deleteACL(fullName, name);
                return null;
            }
        });
    }

    /**
     * Add the rights specified in the ACL to the entry for the
     * identifier specified in the ACL. If an entry for the identifier
     * doesn't already exist, add one.
     * 
     * @param acl the identifer and rights to add
     * @exception MessagingException if the server doesn't support the
     *                ACL extension
     */
    public void addRights(ACL acl) throws MessagingException {
        setACL(acl, '+');
    }

    /**
     * Remove the rights specified in the ACL from the entry for the
     * identifier specified in the ACL.
     * 
     * @param acl the identifer and rights to remove
     * @exception MessagingException if the server doesn't support the
     *                ACL extension
     */
    public void removeRights(ACL acl) throws MessagingException {
        setACL(acl, '-');
    }

    /**
     * Get all the rights that may be allowed to the given identifier.
     * Rights are grouped per RFC 2086 and each group is returned as an
     * element of the array. The first element of the array is the set
     * of rights that are always granted to the identifier. Later
     * elements are rights that may be optionally granted to the
     * identifier.
     * <p>
     * Note that this method lists the rights that it is possible to assign to
     * the given identifier, <em>not</em> the rights that are actually granted
     * to the given identifier. For the latter, see the <code>getACL</code>
     * method.
     * 
     * @param name the identifier to list rights for
     * @return array of Rights objects representing possible
     *         rights for the identifier
     * @exception MessagingException if the server doesn't support the
     *                ACL extension
     */
    public Rights[] listRights(final String name) throws MessagingException {
        return (Rights[]) doOptionalCommand("ACL not supported", new ProtocolCommand() {

            public Object doCommand(IMAPProtocol p) throws ProtocolException {
                return p.listRights(fullName, name);
            }
        });
    }

    /**
     * Get the rights allowed to the currently authenticated user.
     * 
     * @return the rights granted to the current user
     * @exception MessagingException if the server doesn't support the
     *                ACL extension
     */
    public Rights myRights() throws MessagingException {
        return (Rights) doOptionalCommand("ACL not supported", new ProtocolCommand() {

            public Object doCommand(IMAPProtocol p) throws ProtocolException {
                return p.myRights(fullName);
            }
        });
    }

    private void setACL(final ACL acl, final char mod) throws MessagingException {
        doOptionalCommand("ACL not supported", new ProtocolCommand() {

            public Object doCommand(IMAPProtocol p) throws ProtocolException {
                p.setACL(fullName, mod, acl);
                return null;
            }
        });
    }

    /**
     * Get the attributes that the IMAP server returns with the
     * LIST response.
     * 
     * @since JavaMail 1.3.3
     */
    public synchronized String[] getAttributes() throws MessagingException {
        checkExists();
        if (attributes == null)
            exists();		// do a LIST to set the attributes
        return attributes == null ? new String[0] : (String[]) (attributes.clone());
    }

    /**
     * Use the IMAP IDLE command (see
     * <A HREF="http://www.ietf.org/rfc/rfc2177.txt">RFC 2177</A>),
     * if supported by the server, to enter idle mode so that the server
     * can send unsolicited notifications of new messages arriving, etc.
     * without the need for the client to constantly poll the server.
     * Use an appropriate listener to be notified of new messages or
     * other events. When another thread (e.g., the listener thread)
     * needs to issue an IMAP comand for this folder, the idle mode will
     * be terminated and this method will return. Typically the caller
     * will invoke this method in a loop.
     * <p>
     * The mail.imap.minidletime property enforces a minimum delay before
     * returning from this method, to ensure that other threads have a chance to
     * issue commands before the caller invokes this method again. The default
     * delay is 10 milliseconds.
     * 
     * @exception MessagingException if the server doesn't support the
     *                IDLE extension
     * @exception IllegalStateException if the folder isn't open
     * @since JavaMail 1.4.1
     */
    public void idle() throws MessagingException {
        // ASSERT: Must NOT be called with this folder's
        // synchronization lock held.
        assert !Thread.holdsLock(this);
        synchronized (this) {
            checkOpened();
            Boolean started = (Boolean) doOptionalCommand("IDLE not supported",
                    new ProtocolCommand() {

                        public Object doCommand(IMAPProtocol p) throws ProtocolException {
                            if (idleState == RUNNING) {
                                p.idleStart();
                                idleState = IDLE;
                                return Boolean.TRUE;
                            } else {
                                // some other thread must be running the IDLE
                                // command, we'll just wait for it to finish
                                // without aborting it ourselves
                                try {
                                    // give up lock and wait to be not idle
                                    messageCacheLock.wait();
                                } catch (InterruptedException ex) {}
                                return Boolean.FALSE;
                            }
                        }
                    });
            if (!started.booleanValue())
                return;
        }

        /*
         * We gave up the folder lock so that other threads
         * can get into the folder far enough to see that we're
         * in IDLE and abort the IDLE.
         * 
         * Now we read responses from the IDLE command, especially
         * including unsolicited notifications from the server.
         * We don't hold the messageCacheLock while reading because
         * it protects the idleState and other threads need to be
         * able to examine the state.
         * 
         * We hold the messageCacheLock while processing the
         * responses so that we can update the number of messages
         * in the folder (for example).
         */
        for (;;) {
            Response r = protocol.readIdleResponse();
            try {
                synchronized (messageCacheLock) {
                    if (r == null || protocol == null || !protocol.processIdleResponse(r)) {
                        idleState = RUNNING;
                        messageCacheLock.notifyAll();
                        break;
                    }
                }
            } catch (ConnectionException cex) {
                // Oops, the store or folder died on us.
                throwClosedException(cex);
            } catch (ProtocolException pex) {
                throw new MessagingException(pex.getMessage(), pex);
            }
        }

        /*
         * Enforce a minimum delay to give time to threads
         * processing the responses that came in while we
         * were idle.
         */
        int minidle = ((IMAPStore) store).getMinIdleTime();
        if (minidle > 0) {
            try {
                Thread.sleep(minidle);
            } catch (InterruptedException ex) {}
        }
    }

    /*
     * If an IDLE command is in progress, abort it if necessary,
     * and wait until it completes.
     * ASSERT: Must be called with the message cache lock held.
     */
    void waitIfIdle() throws ProtocolException {
        assert Thread.holdsLock(messageCacheLock);
        while (idleState != RUNNING) {
            if (idleState == IDLE) {
                protocol.idleAbort();
                idleState = ABORTING;
            }
            try {
                // give up lock and wait to be not idle
                messageCacheLock.wait();
            } catch (InterruptedException ex) {}
        }
    }

    /**
     * The response handler. This is the callback routine that is
     * invoked by the protocol layer.
     */
    /*
     * ASSERT: This method must be called only when holding the
     * messageCacheLock.
     * ASSERT: This method must *not* invoke any other method that
     * might grab the 'folder' lock or 'message' lock (i.e., any
     * synchronized methods on IMAPFolder or IMAPMessage)
     * since that will result in violating the locking hierarchy.
     */
    public void handleResponse(Response r) {
        assert Thread.holdsLock(messageCacheLock);

        /*
         * First, delegate possible ALERT or notification to the Store.
         */
        if (r.isOK() || r.isNO() || r.isBAD() || r.isBYE())
            ((IMAPStore) store).handleResponseCode(r);

        /*
         * Now check whether this is a BYE or OK response and
         * handle appropriately.
         */
        if (r.isBYE()) {
            if (opened)		// XXX - accessed without holding folder lock
                cleanup(false);
            return;
        } else if (r.isOK()) {
            return;
        } else if (!r.isUnTagged()) {
            return;	// XXX - should never happen
        }

        /* Now check whether this is an IMAP specific response */
        if (!(r instanceof IMAPResponse)) {
            // Probably a bug in our code !
            // XXX - should be an assert
            out.println("UNEXPECTED RESPONSE : " + r.toString());
            out.println("CONTACT javamail@sun.com");
            return;
        }

        IMAPResponse ir = (IMAPResponse) r;

        if (ir.keyEquals("EXISTS")) { // EXISTS
            int exists = ir.getNumber();
            if (exists <= realTotal)
                // Could be the EXISTS following EXPUNGE, ignore 'em
                return;

            int count = exists - realTotal; // number of new messages
            Message[] msgs = new Message[count];

            // Add 'count' new IMAPMessage objects into the messageCache
            messageCache.addMessages(count);
            int oldtotal = total;	// used in loop below
            realTotal += count;
            total += count;

            // avoid instantiating Message objects if no listeners.
            if (hasMessageCountListener) {
                for (int i = 0; i < count; i++)
                    msgs[i] = messageCache.getMessage(++oldtotal);

                // Notify listeners.
                notifyMessageAddedListeners(msgs);
            }

        } else if (ir.keyEquals("EXPUNGE")) {
            // EXPUNGE response.

            int seqnum = ir.getNumber();
            messageCache.expungeMessage(seqnum);

            // decrement 'realTotal'; but leave 'total' unchanged
            realTotal--;

            if (doExpungeNotification && hasMessageCountListener) {
                // Do the notification here.
                Message[] msgs = { getMessageBySeqNumber(seqnum) };
                notifyMessageRemovedListeners(false, msgs);
            }

        } else if (ir.keyEquals("FETCH")) {
            // The only unsolicited FETCH response that makes sense
            // to me (for now) is FLAGS updates. Ignore any other junk.
            assert ir instanceof FetchResponse : "!ir instanceof FetchResponse";
            FetchResponse f = (FetchResponse) ir;
            // Get FLAGS response, if present
            Flags flags = (Flags) f.getItem(Flags.class);

            if (flags != null) {
                IMAPMessage msg = getMessageBySeqNumber(f.getNumber());
                if (msg != null) {	// should always be true
                    msg._setFlags(flags);
                    notifyMessageChangedListeners(MessageChangedEvent.FLAGS_CHANGED, msg);
                }
            }

        } else if (ir.keyEquals("RECENT")) {
            // update 'recent'
            recent = ir.getNumber();
        }
    }

    /**
     * Handle the given array of Responses.
     * ASSERT: This method must be called only when holding the
     * messageCacheLock
     */
    void handleResponses(Response[] r) {
        for (int i = 0; i < r.length; i++) {
            if (r[i] != null)
                handleResponse(r[i]);
        }
    }

    /**
     * Get this folder's Store's protocol connection.
     * When acquiring a store protocol object, it is important to
     * use the following steps:
     * IMAPProtocol p = null;
     * try {
     * p = getStoreProtocol();
     * // perform the command
     * } catch (WhateverException ex) {
     * // handle it
     * } finally {
     * releaseStoreProtocol(p);
     * }
     */
    protected synchronized IMAPProtocol getStoreProtocol() throws ProtocolException {
        if (connectionPoolDebug) {
            out.println("DEBUG: getStoreProtocol() - " + "borrowing a connection");
        }
        return ((IMAPStore) store).getFolderStoreProtocol();
    }

    /**
     * Throw the appropriate 'closed' exception.
     */
    private synchronized void throwClosedException(ConnectionException cex)
            throws FolderClosedException, StoreClosedException {
        // If it's the folder's protocol object, throw a FolderClosedException;
        // otherwise, throw a StoreClosedException.
        // If a command has failed because the connection is closed,
        // the folder will have already been forced closed by the
        // time we get here and our protocol object will have been
        // released, so if we no longer have a protocol object we base
        // this decision on whether we *think* the folder is open.
        if ((protocol != null && cex.getProtocol() == protocol)
                || (protocol == null && !reallyClosed))
            throw new FolderClosedException(this, cex.getMessage());
        else
            throw new StoreClosedException(store, cex.getMessage());
    }

    /**
     * Return the IMAPProtocol object for this folder.
     * <p>
     * This method will block if necessary to wait for an IDLE command to
     * finish.
     * 
     * @return the IMAPProtocol object used when the folder is open
     */
    private IMAPProtocol getProtocol() throws ProtocolException {
        assert Thread.holdsLock(messageCacheLock);
        waitIfIdle();
        return protocol;
    }

    /**
     * A simple interface for user-defined IMAP protocol commands.
     */
    public static interface ProtocolCommand {

        /**
         * Execute the user-defined command using the supplied IMAPProtocol
         * object.
         */
        public Object doCommand(IMAPProtocol protocol) throws ProtocolException;
    }

    /**
     * Execute a user-supplied IMAP command. The command is executed
     * in the appropriate context with the necessary locks held and
     * using the appropriate <code>IMAPProtocol</code> object.
     * <p>
     * This method returns whatever the <code>ProtocolCommand</code> object's
     * <code>doCommand</code> method returns. If the <code>doCommand</code>
     * method throws a <code>ConnectionException</code> it is translated into a
     * <code>StoreClosedException</code> or <code>FolderClosedException</code>
     * as appropriate. If the <code>doCommand</code> method throws a
     * <code>ProtocolException</code> it is translated into a
     * <code>MessagingException</code>.
     * <p>
     * The following example shows how to execute the IMAP NOOP command.
     * Executing more complex IMAP commands requires intimate knowledge of the
     * <code>com.sun.mail.iap</code> and <code>com.sun.mail.imap.protocol</code>
     * packages, best acquired by reading the source code.
     * <p>
     * <blockquote>
     * 
     * <pre>
     * import com.sun.mail.iap.*;
     * import com.sun.mail.imap.*;
     * import com.sun.mail.imap.protocol.*;
     * 
     * ...
     * 
     * IMAPFolder f = (IMAPFolder)folder;
     * Object val = f.doCommand(new IMAPFolder.ProtocolCommand() {
     * public Object doCommand(IMAPProtocol p)
     * 		throws ProtocolException {
     *     p.simpleCommand("NOOP", null);
     *     return null;
     * }
     * });
     * </pre>
     * 
     * </blockquote>
     * <p>
     * Here's a more complex example showing how to use the proposed IMAP SORT
     * extension:
     * <p>
     * 
     * <pre>
     * <blockquote>
     * import com.sun.mail.iap.*;
     * import com.sun.mail.imap.*;
     * import com.sun.mail.imap.protocol.*;
     * 
     * ...
     * 
     * IMAPFolder f = (IMAPFolder)folder;
     * Object val = f.doCommand(new IMAPFolder.ProtocolCommand() {
     * public Object doCommand(IMAPProtocol p)
     * 		throws ProtocolException {
     *     // Issue command
     *     Argument args = new Argument();
     *     Argument list = new Argument();
     *     list.writeString("SUBJECT");
     *     args.writeArgument(list);
     *     args.writeString("UTF-8");
     *     args.writeString("ALL");
     *     Response[] r = p.command("SORT", args);
     *     Response response = r[r.length-1];
     * 
     *     // Grab response
     *     Vector v = new Vector();
     *     if (response.isOK()) { // command succesful 
     * 	for (int i = 0, len = r.length; i < len; i++) {
     * 	    if (!(r[i] instanceof IMAPResponse))
     * 		continue;
     * 
     * 	    IMAPResponse ir = (IMAPResponse)r[i];
     * 	    if (ir.keyEquals("SORT")) {
     * 		String num;
     * 		while ((num = ir.readAtomString()) != null)
     * 		    System.out.println(num);
     * 		r[i] = null;
     * 	    }
     * 	}
     *     }
     * 
     *     // dispatch remaining untagged responses
     *     p.notifyResponseHandlers(r);
     *     p.handleResult(response);
     * 
     *     return null;
     * }
     * });
     * </pre>
     * 
     * </blockquote>
     */
    public Object doCommand(ProtocolCommand cmd) throws MessagingException {
        try {
            return doProtocolCommand(cmd);
        } catch (ConnectionException cex) {
            // Oops, the store or folder died on us.
            throwClosedException(cex);
        } catch (ProtocolException pex) {
            throw new MessagingException(pex.getMessage(), pex);
        }
        return null;
    }

    public Object doOptionalCommand(String err, ProtocolCommand cmd) throws MessagingException {
        try {
            return doProtocolCommand(cmd);
        } catch (BadCommandException bex) {
            throw new MessagingException(err, bex);
        } catch (ConnectionException cex) {
            // Oops, the store or folder died on us.
            throwClosedException(cex);
        } catch (ProtocolException pex) {
            throw new MessagingException(pex.getMessage(), pex);
        }
        return null;
    }

    public Object doCommandIgnoreFailure(ProtocolCommand cmd) throws MessagingException {
        try {
            return doProtocolCommand(cmd);
        } catch (CommandFailedException cfx) {
            return null;
        } catch (ConnectionException cex) {
            // Oops, the store or folder died on us.
            throwClosedException(cex);
        } catch (ProtocolException pex) {
            throw new MessagingException(pex.getMessage(), pex);
        }
        return null;
    }

    protected Object doProtocolCommand(ProtocolCommand cmd) throws ProtocolException {
        synchronized (this) {
            /*
             * Check whether we have a protocol object, not whether we're
             * opened, to allow use of the exsting protocol object in the
             * open method before the state is changed to "opened".
             */
            if (protocol != null) {
                synchronized (messageCacheLock) {
                    return cmd.doCommand(getProtocol());
                }
            }
        }

        // only get here if using store's connection
        IMAPProtocol p = null;

        try {
            p = getStoreProtocol();
            return cmd.doCommand(p);
        } finally {
            releaseStoreProtocol(p);
        }
    }

    /**
     * Release the store protocol object. If we borrowed a protocol
     * object from the connection pool, give it back. If we used our
     * own protocol object, nothing to do.
     */
    protected synchronized void releaseStoreProtocol(IMAPProtocol p) {
        if (p != protocol)
            ((IMAPStore) store).releaseFolderStoreProtocol(p);
        else {
            // XXX - should never happen
            if (debug)
                out.println("DEBUG: releasing our protocol as store protocol?");
        }
    }

    /**
     * Release the protocol object.
     * ASSERT: This method must be called only when holding the
     * messageCacheLock
     */
    private void releaseProtocol(boolean returnToPool) {
        if (protocol != null) {
            protocol.removeResponseHandler(this);

            if (returnToPool)
                ((IMAPStore) store).releaseProtocol(this, protocol);
            else {
                protocol.disconnect();	// make sure it's disconnected
                ((IMAPStore) store).releaseProtocol(this, null);
            }
            protocol = null;
        }
    }

    /**
     * Issue a noop command for the connection if the connection has not been
     * used in more than a second. If <code>keepStoreAlive</code> is true,
     * also issue a noop over the store's connection.
     * ASSERT: This method must be called only when holding the
     * messageCacheLock
     */
    private void keepConnectionAlive(boolean keepStoreAlive) throws ProtocolException {

        if (System.currentTimeMillis() - protocol.getTimestamp() > 1000) {
            waitIfIdle();
            protocol.noop();
        }

        if (keepStoreAlive && ((IMAPStore) store).hasSeparateStoreConnection()) {
            IMAPProtocol p = null;
            try {
                p = ((IMAPStore) store).getFolderStoreProtocol();
                if (System.currentTimeMillis() - p.getTimestamp() > 1000)
                    p.noop();
            } finally {
                ((IMAPStore) store).releaseFolderStoreProtocol(p);
            }
        }
    }

    /**
     * Get the message object for the given sequence number. If
     * none found, null is returned.
     * ASSERT: This method must be called only when holding the
     * messageCacheLock
     */
    IMAPMessage getMessageBySeqNumber(int seqnum) {
        return messageCache.getMessageBySeqnum(seqnum);
    }

    private boolean isDirectory() {
        return ((type & HOLDS_FOLDERS) != 0);
    }
}

/**
 * An object that holds a Message object
 * and reports its size and writes it to another OutputStream
 * on demand. Used by appendMessages to avoid the need to
 * buffer the entire message in memory in a single byte array
 * before sending it to the server.
 */
class MessageLiteral implements Literal {

    private Message msg;
    private int msgSize = -1;
    private byte[] buf;		// the buffered message, if not null

    public MessageLiteral(Message msg, int maxsize) throws MessagingException, IOException {
        this.msg = msg;
        // compute the size here so exceptions can be returned immediately
        LengthCounter lc = new LengthCounter(maxsize);
        OutputStream os = new CRLFOutputStream(lc);
        msg.writeTo(os);
        os.flush();
        msgSize = lc.getSize();
        buf = lc.getBytes();
    }

    public int size() {
        return msgSize;
    }

    public void writeTo(OutputStream os) throws IOException {
        // the message should not change between the constructor and this call
        try {
            if (buf != null)
                os.write(buf, 0, msgSize);
            else {
                os = new CRLFOutputStream(os);
                msg.writeTo(os);
            }
        } catch (MessagingException mex) {
            // exceptions here are bad, "should" never happen
            throw new IOException("MessagingException while appending message: " + mex);
        }
    }
}

/**
 * Count the number of bytes written to the stream.
 * Also, save a copy of small messages to avoid having to process
 * the data again.
 */
class LengthCounter extends OutputStream {

    private int size = 0;
    private byte[] buf;
    private int maxsize;

    public LengthCounter(int maxsize) {
        buf = new byte[8192];
        this.maxsize = maxsize;
    }

    public void write(int b) {
        int newsize = size + 1;
        if (buf != null) {
            if (newsize > maxsize && maxsize >= 0) {
                buf = null;
            } else if (newsize > buf.length) {
                byte newbuf[] = new byte[Math.max(buf.length << 1, newsize)];
                System.arraycopy(buf, 0, newbuf, 0, size);
                buf = newbuf;
                buf[size] = (byte) b;
            } else {
                buf[size] = (byte) b;
            }
        }
        size = newsize;
    }

    public void write(byte b[], int off, int len) {
        if ((off < 0) || (off > b.length) || (len < 0) || ((off + len) > b.length)
                || ((off + len) < 0)) {
            throw new IndexOutOfBoundsException();
        } else if (len == 0) {
            return;
        }
        int newsize = size + len;
        if (buf != null) {
            if (newsize > maxsize && maxsize >= 0) {
                buf = null;
            } else if (newsize > buf.length) {
                byte newbuf[] = new byte[Math.max(buf.length << 1, newsize)];
                System.arraycopy(buf, 0, newbuf, 0, size);
                buf = newbuf;
                System.arraycopy(b, off, buf, size, len);
            } else {
                System.arraycopy(b, off, buf, size, len);
            }
        }
        size = newsize;
    }

    public void write(byte[] b) throws IOException {
        write(b, 0, b.length);
    }

    public int getSize() {
        return size;
    }

    public byte[] getBytes() {
        return buf;
    }
}
